Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add assertion framework support to client authentication #336

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

dhensby
Copy link
Contributor

@dhensby dhensby commented Mar 14, 2025

Summary

I'm looking to implement client assertion support. At the moment I'm leaning to keeping as much of this in
user-land code, but maybe we should have a way to register client authentication plugins (in a similar way that
we do with grant types). I think that might take quite a refactor (and breaking changes) to allow for that.

At the moment this is just a draft to check what the minimum interface is to get this working in user-land.


Some way to inject a body parser/interpreter is needed because the assertions contain most of the relevant request data that is typically just a property in the request body and most of the library makes an assumption that all the data is props in the body.

Linked issue(s)

N/A

Involved parts of the project

Client authentication

Added tests?

todo

OAuth2 standard

Example

This would be implemented something like this to support client assertions with JWT:

getting a token:

        const token = await server.token(request, response, {
            requestProcessor: (incoming: OAuth2Server.Request) => {
                // determine if this is a request we can process (ie: a client assertion)
                if (isClientAssertionRequest(incoming)) {
                    // just read out the JWT data - this isn't being verified as genuine at this point, that comes later - we just want to know who this request is *claiming* to be.
                    const { scope, sub: client_id } = decodeJwt<{ scope?: string }>(incoming.body.client_assertion);
                    return {
                        client_id,
                        client_assertion: incoming.body.client_assertion,
                        client_assertion_type: incoming.body.client_assertion_type,
                        grant_type: incoming.body.grant_type,
                        code_verifier: incoming.body.code_verifier,
                        scope,
                    };
                }
                return incoming.body as TokenRequest;
            },
        });

Implementing getClientFromAssertion:

    async getClientFromAssertion(assertion: OAuth2Server.AssertionCredential): Promise<OAuth2Server.Client | OAuth2Server.Falsey> {
        // verify we can handle this assertion type
        if (assertion.clientAssertionType !== 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer') {
            throw new InvalidClientError('client_assertion_type not supported');
        }
        // first thing is to try to extract the client id from the assertion
        // if a clientId is present, check it matches (as per spec - it's optional but must match if supplied)
        // then find their key and validate the assertion
        const { sub: clientId } = decodeJwt(assertion.clientAssertion);
        if (!clientId) {
            throw new InvalidClientError('client_assertion malformed');
        }
        if (assertion.clientId && assertion.clientId !== clientId) {
            throw new InvalidClientError('client_id mismatch');
        }
        // somehow get the client and their keys
        const client = await getClient(clientId);
        const jwks = createLocalJWKSet({ keys: client.keys });
        // actually verify the assertion is genuine/trusted
        await jwtVerify(assertion.clientAssertion, jwks, {
            requiredClaims: [
                'iss',
                'sub',
                'aud',
                'exp',
            ],
            maxTokenAge: 60,
            audience: ['https://example.com'],
        }).catch((e) => {
            return Promise.reject(new InvalidClientError(e as Error));
        });
        // any other validation can be performed here (eg: issuer is acceptable, audience, etc)
        return client;
    }

@dhensby dhensby force-pushed the pulls/assertion-framework-support branch 2 times, most recently from 5e54253 to 80571a7 Compare April 11, 2025 09:23
@dhensby dhensby force-pushed the pulls/assertion-framework-support branch from 80571a7 to 95cbfc5 Compare April 11, 2025 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant